<template>
  <div class="x-file-explorer-lite">
    <SplitPane v-model="offset" style="background-color: #fff" :hide-left="!showTree">
      <!--左侧导航树-->
      <template #left>
        <div class="x-file-explorer-lite-left">
          <FileExplorerLiteNavTree
            ref="tree"
            :fetch-api="treeFetchApi"
            :delete-api="treeDeleteApi"
            :save-api="treeSaveApi"
            :current-key="dirId"
            :update-api="treeUpdateApi"
            @node-updated="nodeUpdated"
            @node-added="nodeAdded"
            @create-new-node="createNewNode"
            @remove-new-node="removeNewNode"
            @node-click="onNodeClick"
            @node-deleted="nodeDeleted" />
        </div>
      </template>
      <!--右侧文件浏览区-->
      <template #right>
        <div class="x-file-explorer-lite-right">
          <!--顶部工具条-->
          <div ref="toolbarScroller" class="tool-bar-scroller">
            <div ref="toolBar" class="x-file-explorer-lite-toolbar">
              <slot name="toolbar" :files="files" :selected-rows="selectedRows" :current-row="currentRow">
                <slot name="left" :instance="this"></slot>
                <CreateFileButton v-show="buttons.includes('新建')" />
                <ElButton
                  v-show="buttons.includes('上传')"
                  size="mini"
                  icon="el-icon-upload"
                  type="primary"
                  @click="btnUpload">
                  上传
                </ElButton>
                <DownloadFileButton
                  v-show="buttons.includes('下载')"
                  ref="downloadFileButton"
                  :selected-rows="selectedRows" />
                <DeleteFileButton
                  v-show="buttons.includes('删除')"
                  ref="deleteFileButton"
                  :api="deleteApi"
                  :selected-rows="selectedRows" />
                <slot name="right" :instance="this"></slot>
                <div v-if="searchable" class="search-wrapper">
                  <ElInput
                    v-model="searchText"
                    placeholder="请输入内容"
                    prefix-icon="el-icon-search"
                    clearable
                    size="mini"
                    class="search-input"
                    @clear="reload"
                    @keyup.enter.native="reload" />
                  <ElButton size="mini" type="primary" icon="el-icon-search" @click="reload">搜索</ElButton>
                </div>
              </slot>
              <!-- 当提供外部提供 toolbar 插槽内容时必须创建下面的插件,因为右键菜单可能会用到这些插件 -->
              <div v-if="!!$scopedSlots.toolbar" style="display: none">
                <DownloadFileButton
                  v-show="buttons.includes('下载')"
                  ref="downloadFileButton"
                  :selected-rows="selectedRows" />
                <DeleteFileButton
                  v-show="buttons.includes('删除')"
                  ref="deleteFileButton"
                  :api="deleteApi"
                  :selected-rows="selectedRows" />
              </div>
            </div>
          </div>
          <!--面包屑-->
          <XMemoryPathBreadcrumb
            v-if="memory"
            v-show="showBreadcrumb"
            v-model="breadCrumbs"
            :loading="loading"
            :default-val="[{ id: rootFolderId, name: '全部文件' }]" />
          <XRouterPathBreadcrumb
            v-else
            v-show="showBreadcrumb"
            v-model="breadCrumbs"
            :route-path="$route.path"
            :default-val="[{ id: rootFolderId, name: '全部文件' }]" />
          <!--表格区-->
          <ElTable
            ref="table"
            :data="files"
            highlight-current-row
            height="auto"
            v-bind="$attrs"
            @row-contextmenu="showMenu"
            @row-click="toggleRowSelection"
            @selection-change="selectionChange">
            <slot :open-file="openFile" :get-file-icon="getFileIcon">
              <TableColumn v-if="selectionColumn" type="selection" align="center" width="55" />
              <TableColumn v-if="indexColumn" type="index" align="center" label="序号" width="80" />
              <TableColumn label="文件名称" prop="name">
                <template #default="{ row }">
                  <template v-if="row.edit">
                    <div style="display: flex; align-items: center">
                      <ElInput
                        ref="etName"
                        v-model="fileName"
                        size="mini"
                        style="max-width: 200px"
                        @keyup.native="event => onFileNameInputKeyUp(event, row)" />
                      <i
                        class="el-icon-close"
                        title="取消"
                        style="margin: 0 10px; color: #fd0b0b; font-weight: bold; cursor: pointer"
                        @click.stop="exitEditFile(row)"></i>
                      <i
                        class="el-icon-check"
                        title="确定"
                        style="color: #00e000; font-weight: bold; cursor: pointer"
                        @click.stop="updateFileName(row)"></i>
                    </div>
                  </template>
                  <div v-else class="flex-row vertical-center">
                    <div class="file-icon">
                      <i :style="{ backgroundImage: `url(${getFileIcon(row)})` }"></i>
                    </div>
                    <span class="file-name ellipsis-1" @click.stop="openFile(row)">
                      {{ row[nameProp] }}
                    </span>
                  </div>
                </template>
              </TableColumn>
              <TableColumn label="上传时间(上传人)" align="center" prop="updater" width="280">
                <template #default="{ row }">
                  <span style="margin-left: 8px">
                    {{ row.id === -1 ? '-' : `${row.updatedAt || row.createdAt}(${row.creator.name})` }}
                  </span>
                </template>
              </TableColumn>
              <TableColumn label="文件大小" align="center" prop="size" width="280">
                <template #default="{ row }">
                  {{ formatSize(row.size) }}
                </template>
              </TableColumn>
            </slot>
            <template #empty>
              <template v-if="tableStatus === TABLE_STATUS.loading">
                <span class="x-file-explorer-lite-loading"
                  ><i class="x-file-explorer-lite-status-icon el-icon-loading"></i> 加载中...</span
                >
              </template>
              <template v-else-if="tableStatus === TABLE_STATUS.error">
                <ElButton
                  class="x-file-explorer-lite-error"
                  icon="x-file-explorer-lite-status-icon fa fa-frown-o"
                  @click="fetchFile">
                  加载失败，点击重试！
                </ElButton>
              </template>
              <template v-else>暂无数据</template>
            </template>
          </ElTable>
          <!--分页-->
          <ElPagination
            :current-page="page.page"
            :page-size="page.limit"
            :page-sizes="pageSizes"
            :total="page.total"
            style="text-align: right; background-color: #fff"
            background
            :layout="`total, prev, pager, next ${pageSizes.length > 0 ? ',sizes' : ''}`"
            @current-change="currentChange" />
          <!-- 拖拽遮罩 -->
          <div v-show="showMask" class="drag-mask"></div>
          <!--父容器 resize 监听-->
          <ResizeObserver @notify="computeToolBarWidth" />
        </div>
      </template>
    </SplitPane>
    <!--右键菜单-->
    <ContextMenu ref="contextMenu" :list="menus" @on-item-click="onMenuItemClick" />
    <!--文件上传-->
    <FastFileUploader
      ref="uploader"
      :params="mergedUploadParams"
      :file-add="checkCanUpload"
      @success="uploadFileSuccess" />
    <!--复制和移动文件插件-->
    <CopyMovePlugin ref="copyMovePlugin" :api="treeFetchApi" />
    <!--重命名插件-->
    <RenameFilePlugin ref="renameFilePlugin" />
  </div>
</template>

<script>
  import FileExplorerLiteNavTree from '@/components/x-file-explorer-lite/XFileExplorerLiteNavTree'
  import XMemoryPathBreadcrumb from '@/components/x-file-explorer-lite/XMemoryPathBreadcrumb'
  import XRouterPathBreadcrumb from '@/components/x-file-explorer-lite/XRouterPathBreadcrumb'
  import page from '@/vue/mixin/page'
  import { openFile } from '@/api/file-view'
  import {
    getFilePrefix,
    getEleWidth,
    getBreadcrumbForTreeNode,
    getPathQueryForFolder,
    getPathQueryForTreeNode,
    getPathFromBreadCrumbs,
    isFunction,
    isString,
    formatSize
  } from '@web-utils/core'
  import { getFileIcon } from '@/util'
  import { assignReactiveProperties } from '@/util/reactive'
  import FastFileUploader from '@/components/uploader/FastFileUploader'
  import DeleteFileButton from '@/components/x-file-explorer-lite/plugins/DeleteFileButton'
  import DownloadFileButton from '@/components/x-file-explorer-lite/plugins/DownloadFileButton'
  import CreateFileButton from '@/components/x-file-explorer-lite/plugins/CreateFileButton'
  import RenameFilePlugin from '@/components/x-file-explorer-lite/plugins/RenameFilePlugin'
  import CopyMovePlugin from '@/components/x-file-explorer-lite/plugins/CopyMovePlugin'

  const TABLE_STATUS = Object.freeze({ loading: 1, error: -1, empty: 2, success: 0 })

  function menuFilter(menuItem, file) {
    if (!menuItem.type) {
      return true
    }
    if (menuItem.children) {
      menuItem.children = menuItem.children.filter(child => menuFilter(child, file))
    }
    if (file?.type === 'directory') {
      return menuItem.type.includes('directory')
    } else {
      return menuItem.type.includes('file') || menuItem.type.includes(file?.type)
    }
  }

  export default {
    name: 'XFileExplorerLite',
    components: {
      CopyMovePlugin,
      RenameFilePlugin,
      CreateFileButton,
      DownloadFileButton,
      DeleteFileButton,
      FastFileUploader,
      XMemoryPathBreadcrumb,
      XRouterPathBreadcrumb,
      FileExplorerLiteNavTree
    },
    mixins: [page],
    provide() {
      return {
        explorer: this
      }
    },
    props: {
      fetchApi: Function,
      addApi: Function,
      updateApi: Function,
      deleteApi: Function,
      rootFolderId: [String, Number],
      treeSaveApi: Function,
      treeUpdateApi: Function,
      treeDeleteApi: Function,
      treeFetchApi: Function,
      showTree: {
        type: Boolean,
        default: false
      },
      showBreadcrumb: {
        type: Boolean,
        default: true
      },
      rules: {
        type: Array,
        default: () => []
      },
      menu: {
        type: [Array, Function],
        default: () => [
          {
            text: '查看',
            action: 'view',
            icon: 'el-icon-view',
            type: ['file']
          },
          {
            text: '编辑',
            action: 'edit',
            icon: 'el-icon-lx-edit',
            type: ['none']
          },
          {
            text: '重命名',
            action: 'rename',
            icon: 'el-icon-edit-outline',
            type: ['directory', 'file']
          },
          {
            text: '复制',
            action: 'copy',
            icon: 'el-icon-document-copy',
            type: ['directory', 'file']
          },
          {
            text: '移动',
            action: 'move',
            icon: 'el-icon-d-arrow-right',
            type: ['directory', 'file']
          },
          {
            text: '下载',
            action: 'download',
            icon: 'el-icon-download',
            type: ['file']
          },
          {
            text: '删除',
            action: 'del',
            icon: 'el-icon-circle-close',
            type: ['directory', 'file']
          }
        ]
      },
      fileNameProp: {
        type: [String, Function],
        default: 'filename'
      },
      selectionColumn: Boolean,
      indexColumn: Boolean,
      memory: Boolean,
      searchable: {
        type: Boolean,
        default: true
      },
      nameEditMode: {
        type: String,
        default: 'inline'
      },
      uploadParams: {
        type: Object,
        default: () => ({
          bizId: '',
          bizName: '',
          directoryId: ''
        })
      },
      createFolderParams: {
        type: Object,
        default: () => ({
          folderUser: '',
          folderCategory: '',
          ouId: -1
        })
      },
      pageSizes: {
        type: Array,
        default: () => []
      },
      limit: {
        type: Number,
        default: 25
      },
      buttons: {
        type: Array,
        default: () => ['新建', '上传', '下载', '删除']
      },
      disabled: Boolean
    },
    data() {
      return {
        TABLE_STATUS,
        tableStatus: TABLE_STATUS.empty,
        breadCrumbs: [],
        selectedRows: [],
        files: [],
        currentRow: null,
        dirId: null,
        fileName: '',
        searchText: '',
        renaming: false,
        loading: false,
        offset: '250px',
        folder: {},
        showMask: false
      }
    },
    computed: {
      mergedUploadParams() {
        return { ...this.uploadParams, directoryId: this.currentFolderId }
      },
      menus() {
        const menu = isFunction(this.menu) ? this.menu(this.currentRow) : this.menu
        return menu.filter(value => menuFilter(value, this.currentRow))
      },
      currentFolderId() {
        return this.dirId || this.rootFolderId
      },
      nameProp() {
        if (isString(this.fileNameProp)) {
          return this.fileNameProp
        }
        if (isFunction(this.fileNameProp)) {
          return this.fileNameProp()
        }
        return 'filename'
      }
    },
    watch: {
      breadCrumbs(newVal) {
        const last = newVal[newVal.length - 1]
        this.$emit('root-id', newVal[0].id)
        this.$refs.tree?.setCurrentKey(last.id)
        this.page.page = 1
        this.dirId = last.id
        this.fetchFile()
      },
      showTree: {
        immediate: true,
        handler(newVal) {
          if (newVal) {
            this.offset = '250px'
          } else {
            this.offset = '0'
          }
        }
      },
      limit: {
        immediate: true,
        handler(newVal) {
          this.page.limit = newVal
        }
      }
    },
    created() {
      this.addRules(this.rules)
    },
    async mounted() {
      await this.$nextTick()
      const dropEle = this.$refs.table.$el
      const uploader = this.$refs.uploader.uploader
      uploader.assignDrop(dropEle)
      uploader.on('dragover', this.onDragOver)
      uploader.on('dragleave', this.onDragLeave)
      uploader.on('drop', this.onDrop)
    },
    beforeDestroy() {
      const dropEle = this.$refs.table.$el
      const uploader = this.$refs.uploader.uploader
      uploader.off('dragover', this.onDragOver)
      uploader.off('dragleave', this.onDragLeave)
      uploader.off('drop', this.onDrop)
      uploader.unAssignDrop(dropEle)
    },
    methods: {
      formatSize,
      getFileIcon,
      onDragOver() {
        this.showMask = true
      },
      onDragLeave() {
        this.showMask = false
      },
      onDrop() {
        this.showMask = false
      },
      btnUpload() {
        this.$refs.uploader.openUploader()
      },
      checkCanUpload() {
        return true
      },
      async uploadFileSuccess() {
        await this.fetchFile()
      },
      keyup(event) {
        if (event && (event.type === 'click' || event.keyCode === 13)) {
          this.fetchFile()
        }
      },
      createNewNode(newChild) {
        if (newChild && newChild.parentId === this.folder.id) {
          const newFolder = {
            id: -1,
            [this.nameProp]: '新建文件夹',
            type: 'directory'
          }
          this.files.unshift(newFolder)
          this.currentRow = newFolder
        }
      },
      removeNewNode() {
        if (this.files?.[0]?.id === -1) {
          this.files.shift()
        }
      },
      nodeAdded(data) {
        const newFolder = this.files?.[0]
        if (newFolder && newFolder.id === -1) {
          assignReactiveProperties(newFolder, { ...data, [this.nameProp]: data.folderName, edit: false })
        }
      },
      async nodeUpdated(data) {
        const breadcrumb = this.breadCrumbs.find(value => value.id === data.id)
        if (breadcrumb) {
          breadcrumb.name = data.name
        }
        const file = this.files.find(value => value.id === data.id)
        if (file) {
          file[this.nameProp] = data[this.nameProp]
        }

        if (!this.memory) {
          const path = getPathFromBreadCrumbs(this.breadCrumbs)
          const query = { ...this.$route.query, path: path || undefined }
          await this.$router.push({ path: this.routePath, query })
        }
      },
      async nodeDeleted(parentNode, data) {
        if (this.folder.id === data.id) {
          if (this.memory) {
            this.breadCrumbs = getBreadcrumbForTreeNode(parentNode)
          } else {
            const path = getPathQueryForTreeNode(parentNode, 1)
            const query = { ...this.$route.query, path: path || undefined }
            await this.$router.push({ path: this.routePath, query })
          }
        } else {
          await this.fetchFile()
        }
      },
      async onNodeClick(data, node) {
        if (this.loading) {
          this.$message.warning('加载中...')
          return
        }
        if (this.dirId === data.id) {
          return
        }
        if (data.edit) {
          return
        }

        if (this.memory) {
          this.breadCrumbs = getBreadcrumbForTreeNode(node)
        } else {
          const path = getPathQueryForTreeNode(node, 1)
          const query = { ...this.$route.query, path: path || undefined }
          await this.$router.push({ path: this.routePath, query })
        }
      },
      showMenu(row, column, event) {
        if (!this.disabled) {
          event.stopPropagation()
          event.preventDefault()
          this.currentRow = row
          this.$refs.table.setCurrentRow(row)
          this.$refs.contextMenu.show(event)
        }
      },
      async onMenuItemClick(item) {
        if (this[item.action] && item.builtin !== false) {
          await this[item.action](this.currentRow)
        } else {
          this.$emit('menu-item-click', item, this.currentRow)
        }
      },
      selectionChange(val) {
        this.selectedRows = val
        this.$emit('selection-change', this.selectedRows)
      },
      toggleRowSelection(row) {
        if (this.renaming) {
          return
        }
        this.$refs.table.toggleRowSelection(row)
      },
      async openFile(row) {
        if (row.type === 'directory') {
          if (this.loading) {
            this.$message.warning('加载中...')
            return
          }
          if (this.memory) {
            this.breadCrumbs.push({ name: row[this.nameProp], id: row.id })
          } else {
            const query = {
              ...this.$route.query,
              path: getPathQueryForFolder(this.$route, { name: row[this.nameProp], id: row.id })
            }
            await this.$router.push({ path: this.routePath, query })
          }
        } else {
          await this.view(row)
        }
      },
      editFile(file) {
        this.renaming = true
        const editFile = this.files.find(value => value.edit === true)
        if (editFile && editFile.id !== file.id) {
          assignReactiveProperties(editFile, { edit: false })
          assignReactiveProperties(file, { edit: true })
        } else {
          assignReactiveProperties(file, { edit: true })
        }
      },
      exitEditFile(file) {
        this.renaming = false
        file.edit = false
        if (file.id === -1) {
          this.files.shift()
          if (this.$refs.tree) {
            this.$refs.tree.$refs.etTree.removeNewNode()
          }
        }
      },
      download(file) {
        this.$refs.downloadFileButton.downloadFile(file)
      },
      currentChange(page) {
        this.page.page = page
        this.fetchFile()
      },
      reload() {
        this.page.page = 1
        this.fetchFile()
      },
      async fetchFile() {
        if (this.fetchApi) {
          try {
            this.files = []
            this.tableStatus = TABLE_STATUS.loading
            this.loading = true
            const res = await this.fetchApi(this.currentFolderId, this.page, this.searchText)
            const { content: records } = res.data.data
            this.files = records
            this.page.total = res.data.data.totalElements
            this.folder = res.data.data.directory
            this.dirId = this.folder?.id
            this.tableStatus = TABLE_STATUS.success
            if (this.files.length === 0) {
              this.tableStatus = TABLE_STATUS.empty
            }
          } catch (e) {
            console.error(e)
            this.tableStatus = TABLE_STATUS.error
          }
          this.loading = false
        }
      },
      async rename() {
        await this.$refs.renameFilePlugin.rename()
      },
      async onFileNameInputKeyUp(evt, row) {
        // Enter
        if (evt.keyCode === 13) {
          await this.updateFileName(row)
        }
        // Esc
        if (evt.keyCode === 27) {
          this.exitEditFile(row)
        }
      },
      async updateFileName(file) {
        if (!this.fileName) {
          this.$message.warning({ message: '文件名不能为空！', duration: 2000 })
        } else if (file.id === -1) {
          const res = await this.addApi({
            id: -1,
            folderName: this.fileName,
            folderAuthority: 0,
            authInfo: [],
            folderUser: '',
            parentId: this.folder.id,
            ouId: -1,
            baseFolderId: -1,
            ...this.createFolderParams,
            category: this.createFolderParams.folderCategory
          })
          if (this.$refs.tree) {
            const nodes = this.$refs.tree.$refs.etTree.findTreeNode(-1)
            if (nodes && nodes.length) {
              assignReactiveProperties(nodes[0].data, { ...res.data, id: res.data.id, name: res.data.folderName })
              this.$refs.tree.$refs.etTree.registerNode(nodes[0])
            }
          }
          this.reload()
          this.exitEditFile(file)
        } else if (getFilePrefix(this.currentRow[this.nameProp]) === this.fileName) {
          this.$message.warning({ message: '不能与旧文件名相同！', duration: 2000 })
        } else {
          if (this.currentRow.type === 'directory') {
            const res = await this.updateApi(this.currentRow.id, this.fileName)
            if (res.data.code !== 0) {
              this.$message.warning({ message: res.data.msg, duration: 2000 })
              return
            }
            file[this.nameProp] = this.fileName
            if (this.$refs.tree) {
              const nodes = this.$refs.tree.$refs.etTree.findTreeNode(file.id)
              nodes.forEach(node => {
                node.data[this.nameProp] = file[this.nameProp]
              })
            }
          } else {
            const fileName = `${this.fileName}.${this.currentRow.format}`
            const res = await this.updateApi(this.currentRow.id, fileName)
            if (res.data.code !== 0) {
              this.$message.warning({ message: res.data.msg, duration: 2000 })
              return
            }
            file[this.nameProp] = fileName
          }
          this.exitEditFile(file)
        }
      },
      async view(row) {
        const file = row || this.currentRow
        await openFile(file, this.files)
      },
      async del(row) {
        await this.$refs.deleteFileButton.deleteFile([row])
      },
      move() {
        this.$refs.copyMovePlugin.move()
      },
      copy() {
        this.$refs.copyMovePlugin.copy()
      },
      async computeToolBarWidth() {
        await this.$nextTick()
        if (this.$refs.toolBar) {
          // 清空宽度
          this.$refs.toolBar.style.width = ''
          const realWidth = getEleWidth(this.$refs.toolBar)
          const buttons = this.$refs.toolBar.children
          let totalWidth = 0
          for (let i = 0; i < buttons.length; i++) {
            const width = getEleWidth(buttons[i], false, false)
            if (!isNaN(width)) {
              totalWidth += width
            }
          }
          if (realWidth < totalWidth + 10) {
            totalWidth += 10
          }
          // 设置宽度
          this.$refs.toolBar.style.width = `${totalWidth}px`
        }
      }
    }
  }
</script>

<style lang="scss">
  .x-file-explorer-lite {
    height: 100%;

    @include flex-row;
    @include flex-item;

    .tool-bar-scroller {
      width: 100%;
      max-width: 100%;
      overflow-x: auto;
      overflow-y: hidden;
      background-color: white;
    }

    .drag-mask {
      position: absolute;
      z-index: 10;
      width: 100%;
      height: 100%;
      background-color: rgba(0, 0, 0, 0.1);
      border: 1px dashed dodgerblue;
      border-radius: 2px;
      pointer-events: none;
    }

    .file-icon {
      display: inline-block;
      width: 24px;
      min-width: 24px;
      height: 24px;
      margin-right: 6px;
      background-repeat: no-repeat;

      i {
        display: inline-block;
        width: 26px;
        height: 26px;
        background-repeat: no-repeat;
      }
    }

    .file-name {
      cursor: pointer;

      &:hover {
        color: var(--color-primary);
        text-decoration: underline;
      }
    }

    .x-file-explorer-lite-left {
      height: 100%;
      overflow: auto;

      .tree-box {
        height: calc(100% - 40px);

        .et-tree-wrapper {
          height: 100%;
        }
      }
    }

    .x-file-explorer-lite-right {
      @include flex-item;
      @include flex-column;
      position: relative;
      height: 100%;
    }

    .x-file-explorer-lite-toolbar {
      @include flex-row;
      flex-wrap: nowrap;
      align-items: center;
      min-width: 100%;
      height: 40px;
      padding: 0 0 0 10px;
      line-height: 40px;

      > div:last-child,
      > button:last-child {
        margin-right: 10px;
      }
    }

    .search-input {
      width: 200px;
      margin-right: 10px;
    }

    .search-wrapper {
      @include flex-item;
      display: flex;
      flex-wrap: nowrap;
      align-items: center;
      justify-content: flex-end;
      margin-left: 10px;
      text-align: right;
    }

    .egd-split-horizontal .egd-split-trigger-con {
      border-right-color: #e0e1e3;
    }

    .x-file-explorer-lite-status-icon {
      font-size: 20px;
    }

    .x-file-explorer-lite-loading {
      display: inline-flex;
      align-items: center;
    }

    .x-file-explorer-lite-error {
      display: inline-flex;
      align-items: center;
      color: #f00;
      border: 1px solid #fca4a4;

      &:hover {
        background-color: #fff0f0;
      }
    }

    .file-explorer-button {
      margin-left: 10px;

      &:first-child {
        margin-left: 0;
      }

      + .el-button {
        margin-left: 10px;
      }
    }
  }
</style>
